14 Representação Vetorial: Embeddings e Positional Encoding
Em modelos de Processamento de Linguagem Natural (NLP) baseados em Deep Learning, especificamente na arquitetura Transformer, os dados brutos (texto) não podem ser processados diretamente. O primeiro passo crítico é a transformação de tokens discretos (índices inteiros de um vocabulário) em representações vetoriais contínuas e densas, enriquecidas com informações sobre a ordem da sequência.
Este capítulo detalha a arquitetura da camada de entrada, composta por dois subcomponentes principais: Input Embeddings e Positional Encoding.
14.1 1. Input Embeddings (Camada de Embeddings)
A camada de Embeddings atua como uma tabela de consulta (lookup table) aprendível. Enquanto a representação One-Hot Encoding gera vetores esparsos e de altíssima dimensionalidade (tamanho do vocabulário), os Embeddings projetam esses tokens em um espaço vetorial denso de dimensão inferior (\(d_{model}\)), onde a proximidade geométrica reflete a similaridade semântica.
14.1.1 Características Técnicas
- Mapeamento: Cada ID de token \(x\) é mapeado para um vetor \(v \in \mathbb{R}^{d_{model}}\).
- Dimensionalidade (\(d_{model}\)): Um hiperparâmetro da arquitetura (ex: 512 no Transformer original, 4096 no GPT-3).
- Escalonamento de Variância: No artigo original “Attention Is All You Need”, os pesos dos embeddings são multiplicados por \(\sqrt{d_{model}}\). Isso é feito para contrabalancear a magnitude do produto escalar na camada de atenção subsequente, auxiliando na estabilidade dos gradientes durante o treinamento.
14.2 2. Positional Encoding (Codificação Posicional)
Diferente de Redes Neurais Recorrentes (RNNs) ou LSTMs, a arquitetura Transformer não processa dados sequencialmente; ela processa a sequência inteira em paralelo. Consequentemente, o mecanismo de Self-Attention é invariante à permutação. Sem uma injeção explícita de posição, o modelo veria as frases “O cão mordeu o homem” e “O homem mordeu o cão” como idênticas em termos de composição de tokens.
Para resolver isso, injetamos um vetor de Positional Encoding (PE) somando-o ao vetor de Embedding.
14.2.1 Implementação Matemática
A codificação posicional utiliza frequências de ondas senoidais e cossenos de diferentes comprimentos de onda. Para uma posição \(pos\) na sequência e uma dimensão \(i\) dentro do vetor de embedding:
\[ PE_{(pos, 2i)} = \sin\left(\frac{pos}{10000^{2i/d_{model}}}\right) \]
\[ PE_{(pos, 2i+1)} = \cos\left(\frac{pos}{10000^{2i/d_{model}}}\right) \]
Por que esta fórmula? 1. Valores Determinísticos: Não requer parâmetros aprendíveis extras (embora embeddings posicionais aprendíveis também sejam usados em modelos modernos como BERT). 2. Relatividade Linear: Para qualquer deslocamento fixo \(k\), \(PE_{pos+k}\) pode ser representado como uma função linear de \(PE_{pos}\). Isso facilita para o modelo aprender a atender posições relativas (ex: o token anterior ou o próximo). 3. Extrapolação: Permite que o modelo processe sequências mais longas do que as vistas durante o treinamento.
14.3 3. Arquitetura do Fluxo de Dados
A combinação dessas duas camadas resulta na entrada final para os blocos do Transformer. A operação é uma soma elemento a elemento (element-wise addition), seguida geralmente por uma camada de Dropout para regularização.
14.3.1 Diagrama de Fluxo
graph TD
subgraph "Pré-processamento"
RawText[Texto Bruto] --> Tokenizer[Tokenizador]
Tokenizer --> TokenIDs[IDs dos Tokens (Inteiros)]
end
subgraph "Camada de Representação Vetorial"
TokenIDs --> EmbedLayer[Embedding Lookup Table]
EmbedLayer --> Scale[Escalar por sqrt(d_model)]
PosIndex[Índices de Posição 0..N] --> PosEncCalc[Cálculo Seno/Cosseno]
PosEncCalc --> PosVector[Vetor Positional Encoding]
Scale --> Sum((Soma Element-wise))
PosVector --> Sum
Sum --> Dropout[Dropout Layer]
end
Dropout --> TransformerBlock[Bloco Transformer]
style Sum fill:#f9f,stroke:#333,stroke-width:2px
style EmbedLayer fill:#bbf,stroke:#333,stroke-width:2px
style PosEncCalc fill:#bbf,stroke:#333,stroke-width:2px
14.4 4. Implementação de Referência (PyTorch)
Abaixo apresentamos uma implementação robusta e anotada, seguindo as especificações padrão da indústria.
import torch
import torch.nn as nn
import math
class InputEmbeddings(nn.Module):
def __init__(self, d_model: int, vocab_size: int):
"""
Args:
d_model (int): Dimensão do vetor de embedding.
vocab_size (int): Tamanho do vocabulário.
"""
super().__init__()
self.d_model = d_model
self.vocab_size = vocab_size
# Camada de Embedding padrão do PyTorch
self.embedding = nn.Embedding(vocab_size, d_model)
def forward(self, x):
# Escalonamento por sqrt(d_model) conforme paper original
return self.embedding(x) * math.sqrt(self.d_model)
class PositionalEncoding(nn.Module):
def __init__(self, d_model: int, seq_len: int, dropout: float):
"""
Args:
d_model (int): Dimensão do modelo.
seq_len (int): Comprimento máximo da sequência.
dropout (float): Taxa de dropout.
"""
super().__init__()
self.dropout = nn.Dropout(dropout)
# Cria uma matriz de (seq_len, d_model) com zeros
pe = torch.zeros(seq_len, d_model)
# Cria um vetor de posições (0, 1, ... seq_len-1)
# Shape: (seq_len, 1)
position = torch.arange(0, seq_len, dtype=torch.float).unsqueeze(1)
# Termo divisor para as frequências (10000^(2i/d_model))
# Implementado em log-space para estabilidade numérica
div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model))
# Aplica Seno aos índices pares (2i)
pe[:, 0::2] = torch.sin(position * div_term)
# Aplica Cosseno aos índices ímpares (2i+1)
pe[:, 1::2] = torch.cos(position * div_term)
# Adiciona dimensão de batch para facilitar o broadcast na soma: (1, seq_len, d_model)
pe = pe.unsqueeze(0)
# Registra como buffer (não é um parâmetro aprendível, mas faz parte do estado do modelo)
self.register_buffer('pe', pe)
def forward(self, x):
"""
Args:
x: Embeddings de entrada. Shape: (Batch_Size, Seq_Len, d_model)
"""
# Soma o embedding com o positional encoding (até o comprimento da sequência atual)
# x.requires_grad_(False) não é necessário pois pe é buffer, mas a soma mantém o gradiente de x
x = x + self.pe[:, :x.shape[1], :]
return self.dropout(x)
# Exemplo de uso integrado
class TransformerInputLayer(nn.Module):
def __init__(self, vocab_size, d_model, max_len, dropout=0.1):
super().__init__()
self.embeddings = InputEmbeddings(d_model, vocab_size)
self.positional_encoding = PositionalEncoding(d_model, max_len, dropout)
def forward(self, x):
x = self.embeddings(x)
x = self.positional_encoding(x)
return x14.4.1 Análise do Código
- Estabilidade Numérica: No cálculo do
div_term, utilizamostorch.expemath.log. Matematicamente, \(e^{\ln(x)} = x\). Isso evita potências diretas de números muito grandes ou muito pequenos, mantendo a precisão em ponto flutuante. - Buffers: O uso de
register_buffergarante que a matriz de Positional Encoding seja salva junto com o modelo (state_dict), mas não seja atualizada pelo otimizador (Backpropagation), pois é fixa. - Broadcasting: A forma do tensor
peé(1, seq_len, d_model). Isso permite que ele seja somado automaticamente a um batch de entradas(Batch, seq_len, d_model)sem necessidade de duplicação de memória.